Skip to content

S03-03 核心类-枚举

[TOC]

基础

什么是枚举

枚举(Enumeration) 是 Java 1.5 引入的一种特殊的数据类型。它主要用于定义一组固定的常量集合

当你的程序中有一个变量,它的值只能是预先定义好的几个固定的值之一时,就应该使用枚举。例如:

  • 季节:春、夏、秋、冬
  • 订单状态:待支付、已支付、已发货、已完成、已取消
  • 星期:周一到周日

为什么需要枚举

为什么需要枚举(没有枚举的日子):

在枚举出现之前,Java 程序员通常使用 public static final 常量(通常被称为“int 枚举模式”或“String 枚举模式”)来表示常量的集合:

java
public class SeasonConstants {
  public static final int SPRING = 1;
  public static final int SUMMER = 2;
  public static final int AUTUMN = 3;
  public static final int WINTER = 4;
}

这种做法存在几个明显的缺点:

  • 类型不安全:由于它们本质上只是 int 数字,你可以将任何 int 值赋给代表季节的变量(比如赋值为 99),编译器完全不会报错。
  • 可读性差:如果在控制台打印或者调试时,输出的只是数字 12,你很难直接知道它代表的是春天还是夏天。
  • 命名冲突:如果不小心,不同类别的常量可能会混用。

基本语法

枚举的基本语法:

定义枚举

使用 enum 关键字就可以轻松定义一个枚举,完美解决了上述问题。

java
public enum Season {
  SPRING, SUMMER, AUTUMN, WINTER // 语法糖写法,本质是调用枚举类的无参构造器
}
  • 枚举类中的枚举值默认都是 static final 修饰,但不要显式书写。

  • 注意:习惯上,枚举常量的命名全部采用大写字母,多个单词之间用下划线 _ 分隔。

  • 枚举值之间用 , 分隔,最后用 ; 结束(可以忽略 ;,但是推荐用 ; 结束)。

  • 每一个枚举值都是当前类的一个对象。

使用枚举

java
public class EnumDemo {
  public static void main(String[] args) {
    // 1. 声明一个枚举变量并赋值
    Season currentSeason = Season.SPRING;

    // 2. 打印枚举值(默认输出枚举常量的字面量名称)
    System.out.println("当前季节是: " + currentSeason);
    // 输出: 当前季节是: SPRING

    // 3. 类型安全检查:下面这行代码如果取消注释,编译会报错,因为类型不匹配
    // currentSeason = 1;
  }
}

枚举的优势

  1. 绝对的类型安全Season 类型的变量只能被赋值为 Season 枚举中定义的那四个值,传入其他类型或值在编译期就会报错。

  2. 自带良好的可读性:打印出来直接就是具体的英文字符串(如 SPRING),而不是毫无意义的数字。

  3. 命名空间隔离Season.SPRINGWaterSpring.SPRING 互不干扰。

枚举结合 switch

枚举与 switch 语句的完美结合:

在枚举出现之前,switch 语句只能接收整数型 (byte, short, char, int)。Java 1.5 引入枚举后,switch 原生支持了枚举类型。

枚举和 switch 简直是天作之合。使用枚举不仅让 switch 的代码更具可读性,编译器还能帮你做类型检查。

java
public class EnumSwitchDemo {
  public static void main(String[] args) {
    Season current = Season.AUTUMN;

    // 在 switch 中使用枚举时,case 后面直接写枚举常量名即可,不需要加前缀(Season.AUTUMN)
    switch (current) {
      case SPRING:
        System.out.println("春暖花开,适合踏青!");
        break;
      case SUMMER:
        System.out.println("夏日炎炎,适合游泳!");
        break;
      case AUTUMN:
        System.out.println("秋高气爽,适合登高!");
        break;
      case WINTER:
        System.out.println("凛冬将至,适合滑雪!");
        break;
      default:
        System.out.println("未知的季节");
        break;
    }
  }
}

为什么推荐在 switch 中使用枚举?

  • 代码清晰case SPRING:case 1: 的语义明确得多,不需要去查阅文档才知道 1 代表什么。
  • 安全性高:如果传入的不是 Season 类型的变量,代码根本无法编译通过。在一些现代的 Java IDE(如 IntelliJ IDEA)中,如果你漏写了某个枚举值的 case,IDE 甚至会给你友好的警告。

掌握了这些内置方法和 switch 结构,您就已经可以应对大部分基础的枚举开发需求了。但这还不是枚举的完全体,Java 的枚举其实比单纯的常量集合要强大得多。

核心特性

Java 中的枚举(Enum)虽然在语法上看起来像是一种全新的类型,但它在本质上是一个受 JVM 特殊照顾的普通 Java 类。与 C 或 C++ 中仅仅作为整数别名的枚举不同,Java 的枚举拥有非常强大的面向对象特性和底层保障。

以下是 Java 枚举最核心的五大特性:

  1. 绝对的类型安全 (Type Safety)

    这是枚举被引入 Java 的初衷。在没有枚举之前,我们通常使用 intString 常量来代表状态,这极易导致非法的赋值。

    • 严格的编译期检查:当你声明一个方法接收 OrderStatus 枚举时,调用者只能传入该枚举定义好的常量(如 OrderStatus.PAID)。
    • 杜绝非法值:如果你试图传入一个普通的整数或未定义的字符串,代码在编译阶段就会报错,根本无法运行,从而将错误扼杀在摇篮里。
  2. 隐式继承机制 (Implicit Inheritance)

    在 Java 中,使用 enum 关键字定义的类型,在编译后都会自动继承 java.lang.Enum 抽象类。

    • 单继承限制:因为 Java 不支持多重继承,而枚举已经隐式继承了 Enum 类,所以枚举不能再使用 extends 继承其他任何父类
    • 接口扩展:虽然不能继承类,但枚举可以实现一个或多个接口(使用 implements),这使得枚举在策略模式或统一定义行为规范时非常有用。
  3. 天生的单例与线程安全 (Singleton & Thread Safety)

    枚举是 Java 中实现单例模式最完美、最安全的方案,这也是《Effective Java》强烈推荐的做法。

    • 类加载期初始化:枚举常量本质上是该类的 public static final 实例。它们在类被加载时,由类加载器的静态代码块进行初始化。基于 JVM 的类加载机制,这个过程是绝对线程安全的。
    • 私有构造器:枚举的构造器强制为 private。外部代码绝对无法通过 new 关键字来实例化枚举。
    • 免疫反射攻击:Java 的反射机制(Reflection API)在底层源码中做了特殊判断,如果尝试通过反射调用枚举的构造器创建实例,会直接抛出 IllegalArgumentException,彻底堵死了反射破坏单例的漏洞。
  4. 特殊且安全的序列化机制 (Safe Serialization)

    普通的 Java 对象在实现 Serializable 接口后,反序列化时通常会通过反射绕过构造器,从而创建一个全新的对象。这在单例模式中是一个致命缺陷。

    • 按名称序列化:Java 对枚举的序列化和反序列化做了特殊规定。序列化时,仅仅将枚举常量的 name(名称字符串)输出到结果中,而不序列化其内部状态。
    • 防止多实例产生:反序列化时,JVM 会调用 java.lang.Enum.valueOf() 方法,通过名称去内存中查找已经存在的那个唯一实例。这保证了无论怎么序列化和反序列化,枚举对象的内存地址始终是同一个。
  5. 对流程控制的原生支持 (Switch Support)

    枚举与 switch 语句是天作之合。

    • 语法简洁:在 switch 的条件判断中传入枚举变量后,case 分支可以直接写枚举常量的名字(不需要加类名前缀),代码可读性极高。
    • 详尽性检查:在现代的 Java IDE(如 IntelliJ IDEA)或较新的 Java 版本(如结合 Switch 表达式)中,如果你在 switch 中漏写了某个枚举常量的 case 分支,编译器或 IDE 会发出警告,提示你逻辑可能不完整。

通过这五大核心特性可以看出,Java 枚举不仅解决了常量定义的规范问题,还顺带提供了完美的单例实现和极高的安全性。

自定义枚举

在很多其他编程语言中,枚举往往只是一组简单的整数映射,但 Java 的枚举远不止于此。

在 Java 中,枚举的本质是一个真正的类(Class)。这意味着普通类能做的大部分事情,枚举也能做:它可以拥有自己的属性(成员变量)、构造器、方法,甚至可以实现接口。

自定义属性与构造器@

自定义属性与构造器:

在实际开发中,我们往往不只需要一个光秃秃的枚举常量名称(如 UNPAID),还需要给它绑定更多的实际业务含义(比如状态码 0,描述信息 "待支付")。

我们可以通过为枚举添加成员变量和构造器来实现这一点。

【重要前提】:枚举中的常量定义必须放在整个类的第一行(最前面),否则编译器会报错。

java
public enum OrderStatus { 
  // 1. 枚举常量必须放在类的最前面,并调用对应的构造器
  UNPAID(0, "待支付"), 
  PAID(1, "已支付"),
  SHIPPED(2, "已发货"),
  FINISHED(3, "已完成"),
  CANCELED(-1, "已取消");

  // 2. 定义自定义属性(推荐声明为 private final,保证枚举的不可变性)
  private final int code; 
  private final String description; 

  // 3. 定义构造器
  // 【注意】枚举的构造器只能是 private 的(默认也是 private)。
  // 因为枚举是固定的常量集合,不允许在外部通过 new 关键字随意创建。
  private OrderStatus(int code, String description) { 
    this.code = code;
    this.description = description;
  }

  // 4. 提供 Getter 方法(通常不需要 Setter,因为枚举常量在初始化后不应被修改)
  public int getCode() {
    return code;
  }

  public String getDescription() {
    return description;
  }
}

使用示例:

java
public class EnumAdvancedDemo {
  public static void main(String[] args) {
    OrderStatus status = OrderStatus.PAID;
    System.out.println("订单状态码: " + status.getCode());         // 输出: 1
    System.out.println("订单状态描述: " + status.getDescription()); // 输出: 已支付
  }
}

自定义方法

自定义方法(普通方法与静态方法):

除了 Getter 方法,我们还可以根据业务需求在枚举中编写任意的普通方法或静态方法。

getByCode(int code):最常见的静态方法:根据 code 反查枚举

当我们从数据库接收到一个状态码(如 2),我们需要把它转成对应的枚举常量来进行逻辑处理。

java
// 在 OrderStatus 枚举内部添加静态方法
public static OrderStatus getByCode(int code) {
  // 遍历所有枚举常量
  for (OrderStatus status : OrderStatus.values()) {
    if (status.getCode() == code) {
      return status;
    }
  }
  return null; // 或者抛出 IllegalArgumentException 异常
}

包含抽象方法的枚举

包含抽象方法的枚举(多态):

这是枚举一个非常强大的特性(特定于常量的类主体)。如果枚举中的每一个常量都有不同的行为,我们可以直接在枚举中定义一个抽象方法,然后强制每个枚举常量去实现它。

经典案例:计算器操作符

java
public enum Operation {
  PLUS("+") {
    @Override
    public double apply(double x, double y) { return x + y; }
  },
  MINUS("-") {
    @Override
    public double apply(double x, double y) { return x - y; }
  },
  MULTIPLY("*") {
    @Override
    public double apply(double x, double y) { return x * y; }
  },
  DIVIDE("/") {
    @Override
    public double apply(double x, double y) { return x / y; }
  };

  private final String symbol;

  Operation(String symbol) {
    this.symbol = symbol;
  }

  // 定义抽象方法,所有枚举常量都必须实现它
  public abstract double apply(double x, double y);
}

调用方式: Operation.PLUS.apply(5, 3) 会返回 8.0。这就用极其优雅的方式替代了臃肿的 if-elseswitch 逻辑。

实现接口

实现接口:

虽然枚举继承了 java.lang.Enum,无法再继承其他类(单继承局限),但它可以实现一个或多个接口。这在设计模式(如策略模式)或统一接口规范时非常有用。

java
// 定义一个接口
public interface IMessageCode {
  int getCode();
  String getMsg();
}

// 枚举实现接口
public enum ErrorCode implements IMessageCode {
  NOT_FOUND(404, "找不到资源"),
  SERVER_ERROR(500, "服务器内部错误");

  private final int code;
  private final String msg;

  ErrorCode(int code, String msg) {
    this.code = code;
    this.msg = msg;
  }

  // 实现接口方法
  @Override
  public int getCode() { return code; }

  @Override
  public String getMsg() { return msg; }
}

通过给枚举添加属性、方法以及实现接口,Java 枚举已经成为管理复杂业务常量和简单策略模式的绝佳选择。

枚举的常用方法

枚举的常用内置方法

在 Java 中,所有的枚举类在编译后,都会隐式地继承 java.lang.Enum 类(因为 Java 不支持多继承,所以枚举类不能再继承其他类)。正是这个父类,赋予了枚举许多极其便利的内置方法。

name()

public final String name():返回此枚举常量的名称,完全按照其在枚举声明中声明的标识符形式。

  • 无参数
  • 返回String,该枚举常量的精确字符串名称(即代码中声明的变量名)。
  • 抛出:无。

基本示例

获取枚举字面量名称:直接调用以获取底层定义时的源代码变量名字符串。

java
public enum ThreadState {
  NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
}

public class Main {
  public static void main(String[] args) {
    ThreadState state = ThreadState.TIMED_WAITING;

    // 输出 "TIMED_WAITING"
    System.out.println("Current thread state enum name: " + state.name());
  }
}

核心特性

  1. 不可重写性 (Final Modifier)

    java.lang.Enum 源码中,name() 方法被 final 关键字严格修饰。这意味着任何自定义的枚举类都绝对无法重写此方法。Java 语言规范这样设计是为了保证枚举的内部基础设施(特别是基于名称的查找机制 Enum.valueOf(Class, String) 以及 JVM 的序列化/反序列化机制)的绝对安全和对称。枚举的名称在 JVM 类加载时就已经通过构造函数绑定,代表了它的唯一标识,绝不允许在子类中被动态修改或伪装。

    java
    // java.lang.Enum 核心源码片段
    public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
      private final String name;
    
      // 只能由编译器调用的受保护构造函数
      protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
      }
    
      // 强制为 final,杜绝多态覆写
      public final String name() {
        return name;
      }
    }
  2. 与 toString() 的本质语义区别

    尽管 Enum 父类中 toString() 的默认实现也是 return name;,但 toString() 不是 final 的。在高级工程实践中,name() 的语义是“获取代码级别的原生标识符”,通常用于底层框架的反射、动态代理或严格匹配;而 toString() 的语义是“获取人类可读的描述信息”。标准做法是保留 name() 用于逻辑控制,而根据业务需要重写 toString() 用于 UI 展示或日志打印。

    java
    public enum ErrorCode {
      SYS_001 {
        @Override
        public String toString() {
          return "数据库连接超时";
        }
      };
    
      public static void main(String[] args) {
        // name() 永远返回底层标识符 "SYS_001"
        System.out.println(ErrorCode.SYS_001.name());
    
        // toString() 返回重写后的易读文本 "数据库连接超时"
        System.out.println(ErrorCode.SYS_001.toString());
      }
    }

~~注意事项~~:

1. ==持久化与重构灾难 (Refactoring Trap)==:

   这是使用 `name()` 时最常见且致命的业务坑点。许多开发者喜欢将 `enum.name()` 直接存入数据库(如 MySQL 的 VARCHAR 字段),或者在 JSON 序列化时直接以默认形式传给前端。

   **风险点**:一旦后续业务演进,对代码进行了重构(例如因为规范要求,将枚举常量名 `WECHAT_PAY` 修改为 `WX_PAY`),那么数据库中存留的历史数据将**无法反序列化**。调用 `Enum.valueOf()` 时,因为找不到旧名称,会直接抛出 `IllegalArgumentException`,引发线上生产事故。

   ```java
   public enum PayType { WECHAT_PAY, ALI_PAY }

   // 极度危险的用法:依赖 name() 进行持久化
   String dbValue = PayType.WECHAT_PAY.name();

   // 【重构发生】假设后来将 WECHAT_PAY 改为了 WX_PAY

   // 下面这行从 DB 读取历史数据 "WECHAT_PAY" 进行反序列化时,将直接崩溃:
   // Exception in thread "main" java.lang.IllegalArgumentException: No enum constant PayType.WECHAT_PAY
   PayType type = PayType.valueOf("WECHAT_PAY");
  1. 解法:使用自定义映射属性而非 name()

    在工业级开发中,绝对不推荐直接用 name()ordinal() 与外部系统(DB、Redis、前端 API)建立强耦合。最佳实践是为枚举显式定义一个不变的 code(或 value)属性,并将该属性用于数据持久化和交互。这样,无论 Java 层的常量变量名如何重构,只要 code 保持稳定,系统即能保证向下兼容。

    java
    public enum PayType {
      // 变量名随意重构(如改为 WX_PAY),只要 code "1" 不变即可
      WECHAT_PAY(1, "微信支付"),
      ALI_PAY(2, "支付宝");
    
      private final int code;
      private final String desc;
    
      PayType(int code, String desc) {
        this.code = code;
        this.desc = desc;
      }
    
      public int getCode() { return code; }
    
      // 推荐提供一个静态解析方法,替代默认的 valueOf
      public static PayType fromCode(int code) {
        for (PayType type : values()) {
          if (type.code == code) return type;
        }
        throw new IllegalArgumentException("Unknown code: " + code);
      }
    }

~~扩展知识~~:

1. ==编译器自动生成的 valueOf 方法的对称性==:

   虽然 `java.lang.Enum` 提供了一个双参数的 `Enum.valueOf(Class<T> enumType, String name)` 静态方法,但我们在日常代码中直接调用的形如 `ThreadState.valueOf("NEW")` 的单参数方法,实际上是 **`javac` 编译器在编译期动态织入**到你声明的特定枚举类中的。

   这个隐式生成的 `valueOf(String)` 方法强依赖于 `name()` 返回的字符串进行精准比对。正因为 `valueOf` 和 `name` 是一对高度耦合的正反向转换操作,Java 强制要求 `name()` 必须是 `final` 的,以防止开发者破坏这种底层对称性约定。

   ```java
   // 编译器为你隐式生成的代码大致如下(反编译可见):
   public static ThreadState valueOf(String name) {
     return (ThreadState) java.lang.Enum.valueOf(ThreadState.class, name);
   }
  1. 在 EnumMap 和 EnumSet 中的隐式作用

    虽然 EnumMapEnumSet 主要依赖于枚举的 ordinal()(索引位置)来实现极速的位向量/数组操作,但在序列化/反序列化这些高效集合时,JVM 底层依然会使用 name() 记录实际元素,以防跨 JVM 传输或反序列化时,不同机器上加载的枚举类的 ordinal 发生错位。这也是 name() 保证数据一致性的重要防线之一。

toString()

public String toString():返回枚举常量的名称(默认实现与其声明时的标识符完全一致),但专门设计为允许被重写,以为业务提供更具可读性的描述信息。

  • 无参数
  • 返回String,默认情况下返回该枚举常量的精确字符串名称;若被重写,则返回开发者自定义的业务字符串。
  • 抛出:无。

基本示例

默认行为与重写对比:展示未重写时的默认输出,以及如何利用多态性为特定枚举实例重写该方法以提供友好的文案。

java
public enum OrderStatus {
  // 采用默认的 toString() 实现
  CREATED,

  // 为特定枚举实例重写 toString()
  PAID {
    @Override
    public String toString() {
      return "已支付 (等待发货)";
    }
  },

  // 另一种常见模式:通过构造函数配合全局重写
  SHIPPED("已发货");

  private String desc;

  OrderStatus() {} // 给 CREATED 和 PAID 用的无参构造

  OrderStatus(String desc) {
    this.desc = desc;
  }

  // 全局重写(如果实例本身也重写了,实例重写优先级更高)
  @Override
  public String toString() {
    return desc != null ? desc : super.toString();
  }
}

public class Main {
  public static void main(String[] args) {
    System.out.println(OrderStatus.CREATED.toString()); // 输出: CREATED
    System.out.println(OrderStatus.PAID.toString());    // 输出: 已支付 (等待发货)
    System.out.println(OrderStatus.SHIPPED.toString()); // 输出: 已发货
  }
}

核心特性

  1. 非 final 修饰与面向人类友好的设计哲学

    与被 final 严格锁死的 name() 方法不同,java.lang.Enum 源码中的 toString() 是开放的(没有 final 修饰符)。这种设计的核心哲学在于职责分离:name() 承载的是**“系统级标识/元数据”的作用,保证 JVM 在序列化、反射和反序列化时的绝对安全;而 toString() 承载的是“表示层展示”**的作用。Java 允许并鼓励你在需要将枚举直接打印到日志、控制台或简易 UI 时,重写 toString()

    java
    // java.lang.Enum 核心源码片段
    public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
      private final String name;
    
      // 强制为 final,底层标识符不可变
      public final String name() {
        return name;
      }
    
      // 默认实现退化为 name(),但允许子类覆写 (Overridable)
      public String toString() {
        return name;
      }
    }
  2. 基于匿名内部类的实例级重写 (Instance-Specific Method Override)

    在 Java 中,枚举不仅仅是常量集合,它们本质上是功能完整的类(Class),而每一个枚举项实际上是该枚举类的一个静态常量实例(public static final)。当你在某个特定的枚举项后加上 {} 并重写方法时,编译器在底层实际上为你生成了一个继承自该枚举类的匿名子类 (Anonymous Inner Class)。当你调用该枚举项的 toString() 时,利用 Java 的动态绑定机制(多态),会精准路由到该匿名子类的方法实现上。这在实现状态机模式时非常有用。

注意事项

  1. 反序列化陷阱(破坏 valueOf 的对称性)

    这是重写 toString() 后最容易踩坑的地方。开发者通常习惯用 Enum.valueOf(String) 将字符串还原为枚举对象。如果未重写 toString()valueOf(enumObj.toString()) 是安全且能形成闭环的。但是,一旦你重写了 toString(),这层对称性就被打破了!valueOf() 底层**只认 name()**,不认 toString()。将重写后的 toString()结果传给valueOf()会直接导致IllegalArgumentException 运行时崩溃。

    java
    public enum ResponseCode {
      SUCCESS("操作成功");
    
      private final String message;
      ResponseCode(String message) { this.message = message; }
    
      @Override
      public String toString() { return message; }
    }
    
    // 错误示范:依赖重写的 toString() 进行反序列化
    String str = ResponseCode.SUCCESS.toString(); // 得到 "操作成功"
    
    // 下面这行代码会直接抛出异常!
    // Exception in thread "main" java.lang.IllegalArgumentException: No enum constant ResponseCode.操作成功
    ResponseCode code = ResponseCode.valueOf(str);
  2. 隐式调用与日志性能、格式问题

    在字符串拼接、System.out.println 或使用日志框架(如 SLF4J、Logback)的 {} 占位符时,如果不显式调用其他方法,系统会自动隐式调用对象的 toString()。如果你在 toString() 中加入了极其复杂的逻辑(例如包含大量反射或复杂的字符串拼接),这可能会在密集打印日志的高并发场景下成为性能瓶颈。此外,如果你的业务期望在日志中看到英文字符串常量(方便 ELK 日志告警正则匹配),但你把 toString() 重写为了中文描述,就会导致监控系统失效。

    java
    // 日志隐式调用示例
    log.info("当前订单状态为: {}", OrderStatus.PAID);
    // 如果重写了 toString(),日志里打印的将是中文 "已支付 (等待发货)"
    // 这可能破坏原有的基于 "PAID" 关键字的日志监控告警规则

扩展知识

  1. 工业级规范:自定义 getDesc() 替代重写 toString()

    鉴于 toString() 经常在各种不可控的底层框架(如调试器、日志、部分老旧的序列化库)中被隐式调用,在大型企业级应用(如阿里、美团等互联网大厂的代码规范)中,通常不推荐通过重写 toString() 来处理业务逻辑展示

    最佳实践是让枚举实现一个统一的接口(如 BaseEnum),提供专门的 getDesc()getMessage() 方法。这样语义更加明确,toString() 则保留其默认的类名和底层属性值的快照功能,专门用于 Debug。

    java
    public interface IEnum {
      int getCode();
      String getDesc(); // 业务展示强制使用此方法
    }
    
    public enum TradeStatus implements IEnum {
      SUCCESS(200, "交易成功");
    
      private final int code;
      private final String desc;
    
      TradeStatus(int code, String desc) {
        this.code = code;
        this.desc = desc;
      }
    
      @Override public int getCode() { return code; }
      @Override public String getDesc() { return desc; }
    
      // 不重写 toString(),或者重写为 JSON 格式仅供 Debug 用
      @Override
      public String toString() {
        return "TradeStatus{code=" + code + ", desc='" + desc + "'}";
      }
    }

ordinal()

public final int ordinal():返回此枚举常量的序数(即它在枚举声明中的位置,初始常量的序数为零)。

  • 无参数
  • 返回int,该枚举常量在代码中声明的基于 0 的索引位置。
  • 抛出:无。

基本示例

获取枚举声明的索引:直接调用以获取该枚举实例在源代码中排列的绝对位置。

java
public enum ThreadState {
  NEW,           // 0
  RUNNABLE,      // 1
  BLOCKED,       // 2
  WAITING,       // 3
  TIMED_WAITING, // 4
  TERMINATED     // 5
}

public class Main {
  public static void main(String[] args) {
    ThreadState state1 = ThreadState.NEW;
    ThreadState state2 = ThreadState.WAITING;

    System.out.println("NEW 的序数: " + state1.ordinal());     // 输出: 0
    System.out.println("WAITING 的序数: " + state2.ordinal()); // 输出: 3
  }
}

核心特性

  1. 底层不可变性与编译器织入 (Final & Compiler Injection)

    java.lang.Enum 源码中,ordinal() 方法和 name() 方法一样,都被 final 关键字严格修饰,绝对不允许重写。这个值是在枚举类被类加载器加载时,由 JVM 内部机制初始化的。当 javac 编译器编译枚举类时,它会按代码中声明的顺序,依次为每个枚举实例递增分配一个整型值,并通过受保护的构造函数 protected Enum(String name, int ordinal) 注入。这意味着 ordinal 是与源代码物理排列顺序强绑定的底层元数据。

    java
    // java.lang.Enum 核心源码片段
    public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
      private final int ordinal;
    
      // 只能由编译器调用的受保护构造函数
      protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal; // 初始化后不可更改
      }
    
      // 强制为 final,杜绝多态覆写和篡改
      public final int ordinal() {
        return ordinal;
      }
    }
  2. 专为高性能数据结构设计 (Designed for EnumMap/EnumSet)

    Java 官方文档中明确指出:“大多数程序员将永远不会使用此方法。它被设计用于复杂的基于枚举的数据结构,如 EnumSetEnumMap。”

    在底层实现中,EnumMap 根本没有使用哈希表(如 HashMap 复杂的散列、红黑树结构),而是直接维护了一个单纯的 Object[] 数组。当调用 put(enumKey, value) 时,它直接利用 enumKey.ordinal() 作为数组的绝对索引 index 来存放 value。同样,EnumSet 内部使用位向量(Bit Vector,如一个 long 变量即可表示 64 个枚举的状态),利用 ordinal() 进行极致性能的位运算(如 1L << ordinal())。这种设计消除了哈希冲突,实现了绝对的 O(1) 访问时间复杂度。

注意事项

  1. 毁灭性的持久化与重构灾难 (Fragility in Persistence)

    在企业级开发中,ordinal() 的值存入数据库(如 MySQL 的 TINYINT 字段)或作为对外的 API 状态码是极其危险的“反模式”

    风险点ordinal() 强依赖于代码的物理顺序。一旦业务需求变更,开发者在枚举常量之间插入了一个新的状态,或者调整了已有状态的顺序,所有后续的 ordinal() 值都会发生位移。此时,数据库中存储的历史整数将直接映射到错误的枚举状态,导致灾难性的业务逻辑错乱(如把“已发货”错误解析为“已退款”),且这种错误在编译期完全无法被发现。

    java
    // 初始版本 v1.0
    public enum OrderStatus {
      CREATED,  // 0 -> 存入数据库的是 0
      PAID,     // 1 -> 存入数据库的是 1
      SHIPPED   // 2 -> 存入数据库的是 2
    }
    
    // 业务迭代 v2.0:产品经理要求在 PAID 和 SHIPPED 之间加一个 "PACKING(打包中)" 状态
    public enum OrderStatus {
      CREATED,  // 0
      PAID,     // 1
      PACKING,  // 2 (新加入的状态抢占了原来 SHIPPED 的位置)
      SHIPPED   // 3 (原来的 2 变成了 3)
    }
    
    // 灾难发生:从数据库读取到历史数据的状态值 2
    // 业务原本的意思是 SHIPPED(已发货),但现在被映射成了 PACKING(打包中)!
  2. 滥用比较逻辑 (Abusing for Business Logic)

    由于 Enum 实现了 Comparable<E> 接口,其默认的 compareTo 方法也是基于 ordinal() 来比较的。虽然这允许对枚举进行排序,但如果你的业务状态流转并不总是严格按照代码声明的顺序进行,依赖 ordinal() 去判断“状态 A 是否在状态 B 之后”(例如 if(status1.ordinal() > status2.ordinal()))会导致代码极度脆弱。

扩展知识

  1. Effective Java 最佳实践:用实例字段代替序数 (Item 35)

    Joshua Bloch 在《Effective Java》中明确强调:永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例字段中。如果在业务中需要一个与枚举绑定的数字代码(用于数据库存储、前端交互或协议传输),应该显式地声明一个 intString 类型的 code 字段,并在构造函数中硬编码赋值。这样无论枚举的声明顺序如何打乱,业务代码始终坚如磐石。

    java
    // 工业级标准写法:彻底屏蔽 ordinal() 的副作用
    public enum OrderStatus {
      // 显式声明状态码,即使打乱代码顺序,甚至中间插入,10/20/30/40 依然保持不变
      CREATED(10, "已创建"),
      PAID(20, "已支付"),
      PACKING(25, "打包中"), // 新增状态,赋予新 Code,不影响旧数据
      SHIPPED(30, "已发货");
    
      private final int dbCode;
      private final String desc;
    
      OrderStatus(int dbCode, String desc) {
        this.dbCode = dbCode;
        this.desc = desc;
      }
    
      public int getDbCode() {
        return dbCode;
      }
    }

values()

public static [EnumClass][] values():按照枚举常量在源代码中声明的物理顺序,返回包含该枚举类型所有常量的数组

  • 无参数
  • 返回[EnumClass][],一个包含该枚举类所有实例的强类型数组。
  • 抛出:无。

基本示例

遍历枚举的所有状态:最常见的使用场景,通常与增强型 for 循环或 Stream API 结合使用。

java
public enum ThreadState {
  NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
}

public class Main {
  public static void main(String[] args) {
    // 获取并遍历所有枚举实例
    ThreadState[] states = ThreadState.values();

    for (ThreadState state : states) {
      System.out.println("State: " + state.name() + " at index: " + state.ordinal());
    }
  }
}

核心特性

  1. 编译器合成的隐式方法 (Compiler Synthetic Method)

    这是一个极其核心的硬核知识点:如果你去翻阅 java.lang.Enum 的 JDK 源码,你绝对找不到 values() 这个方法。

    values() 是由 javac 编译器在编译期间,为每一个具体的枚举类**动态织入(生成)**的静态方法。因为 java.lang.Enum 是一个泛型抽象类,由于 Java 泛型的类型擦除机制,父类无法在运行时直接实例化出确切类型的数组(即无法直接 new T[])。因此,编译器必须在子类(具体的枚举类)级别生成这个返回具体类型数组的方法。通过 javap -c 反编译 .class 文件,你可以清晰地看到这个合成方法。

    java
    // 实际上 javac 编译器为你生成的代码等价于以下结构:
    public final class ThreadState extends Enum<ThreadState> {
      public static final ThreadState NEW = new ThreadState("NEW", 0);
      public static final ThreadState RUNNABLE = new ThreadState("RUNNABLE", 1);
      // ... 其他实例
    
      // 编译器自动生成的内部私有数组,缓存所有实例
      private static final ThreadState[] $VALUES = { NEW, RUNNABLE, ... };
    
      // 编译器自动生成的 values() 方法
      public static ThreadState[] values() {
        // 注意:这里调用了 clone()!
        return (ThreadState[]) $VALUES.clone();
      }
    }
  2. 防御性拷贝机制 (Defensive Copying)

    正如上方反编译代码所示,values() 方法每次被调用时,并不是直接返回底层的私有静态数组引用(如 $VALUES),而是强制执行了 .clone() 进行防御性拷贝

    这是因为在 Java 中,数组本身是可变的(Mutable)。如果直接返回底层数组的引用,恶意代码或粗心的开发者就可以通过 ThreadState.values()[0] = null; 篡改底层的枚举常量集合,从而彻底破坏枚举的不可变性和线程安全性。通过每次返回克隆的新数组对象,Java 在底层机制上保障了枚举系统的绝对安全。

注意事项

  1. 高频调用导致的内存抖动与性能瓶颈 (Memory Churn)

    由于 values() 每次调用都会在堆内存中申请空间创建一个全新的数组对象,如果在高频热点代码(如循环内部、频繁的网络请求解析中)直接调用 values(),会导致极大的内存开销,并频繁触发 GC(垃圾回收),造成系统性能抖动。

    java
    public enum ErrorCode {
      SYS_ERR(500), BAD_REQ(400), NOT_FOUND(404);
    
      private int code;
      ErrorCode(int code) { this.code = code; }
    
      // 【坑点代码】极度糟糕的实现:每次解析都会创建新数组
      public static ErrorCode fromCodeBad(int code) {
        // 高并发下,这里的 values() 会疯狂产生按需丢弃的数组对象
        for (ErrorCode e : ErrorCode.values()) {
          if (e.code == code) return e;
        }
        return null;
      }
    }
  2. 解法:静态缓存最佳实践 (Static Caching Pattern)

    对于需要频繁遍历枚举或反向查找的场景,绝对的标准做法是在枚举类内部声明一个 private static final 的静态常量来缓存 values() 的结果,或者直接将其缓存到 HashMap 中以实现 O(1)O(1) 的极致查找性能。

    java
    public enum ErrorCode {
      SYS_ERR(500), BAD_REQ(400), NOT_FOUND(404);
    
      private int code;
      ErrorCode(int code) { this.code = code; }
    
      // 【最佳实践】在类加载时只调用一次 values() 并缓存到 Map 中
      private static final Map<Integer, ErrorCode> CODE_MAP = new HashMap<>();
    
      static {
        // 类初始化阶段,安全地遍历一次
        for (ErrorCode e : ErrorCode.values()) {
          CODE_MAP.put(e.code, e);
        }
      }
    
      // O(1) 的无锁高性能查找,且零垃圾对象产生
      public static ErrorCode fromCode(int code) {
        return CODE_MAP.get(code);
      }
    }

扩展知识

  1. 反射环境下的替代方案:Class.getEnumConstants()

    既然 values() 是编译器在具体类中生成的静态方法,那么在泛型或反射环境下(即你手中只有一个 Class<?> 对象,而不知道具体的枚举类型时),你将无法调用静态的 values() 方法。此时,JDK 在 java.lang.Class 类中提供了一个原生的替代方法 getEnumConstants()

    java
    public static <T extends Enum<T>> void printAllValues(Class<T> enumClass) {
      // 错误:泛型类型无法调用具体的静态方法
      // T[] values = T.values();
    
      // 正确:使用 Class 类提供的反射方法
      T[] constants = enumClass.getEnumConstants();
    
      if (constants != null) {
        for (T constant : constants) {
          System.out.println(constant.name());
        }
      }
    }

注:getEnumConstants() 底层同样做了缓存(enumConstants 字段)和防篡改的 clone() 操作,其安全机制和内存开销与 values() 如出一辙。

valueOf()

public static <T extends Enum<T>> T valueOf(Class<T> enumClass, String name):根据指定的枚举 Class 对象和精确的字符串名称,解析并返回对应的枚举常量实例

  • enumClassClass<T>,指定要查询的目标枚举类的 Class 对象。
  • nameString,要获取的枚举常量的精确名称(必须与源代码中声明的标识符完全一致)。
  • 返回T,返回目标枚举类中对应名称的枚举实例。
  • 抛出
  • IllegalArgumentException - 如果指定的枚举类型中找不到对应名称的常量。
  • NullPointerException - 如果传入的 enumClassnamenull

基本示例

原生调用与编译器语法糖对比:展示如何通过 Enum 父类原生方法反射获取,以及日常开发中最常用的单参数编译器合成方法。

java
public enum ThreadState {
  NEW, RUNNABLE, BLOCKED, WAITING
}

public class Main {
  public static void main(String[] args) {
    // 1. 调用 java.lang.Enum 提供的原生双参数方法 (常用于反射、泛型框架)
    ThreadState state1 = Enum.valueOf(ThreadState.class, "RUNNABLE");

    // 2. 调用编译器为 ThreadState 隐式生成的单参数方法 (日常高频用法)
    ThreadState state2 = ThreadState.valueOf("RUNNABLE");

    System.out.println(state1 == state2); // 输出: true,二者等价且指向堆中同一单例
  }
}

核心特性

  1. 双面体:父类原生实现与编译器隐式织入 (Synthetic Sugar)

    这是一个极具迷惑性的设计。在日常代码中,我们调用的 ThreadState.valueOf("NEW") 并不存在于你编写的源代码中,也不是从 java.lang.Enum 继承来的(因为父类的方法需要两个参数)。

    实际上,单参数的 valueOf(String)javac 编译器在编译期动态织入的具体枚举类中的静态方法。它的底层逻辑仅仅是一个“语法糖”,其字节码指令会直接委托给 java.lang.Enum.valueOf(Class, String) 进行实际的解析操作。

    java
    // 编译器为你生成的 ThreadState.class 反编译后的核心逻辑如下:
    public static ThreadState valueOf(String name) {
      // 底层强制委托给父类的双参数方法,并做类型强转
      return (ThreadState) java.lang.Enum.valueOf(ThreadState.class, name);
    }
  2. O(1) 极致性能:底层 Map 缓存机制 (enumConstantDirectory)

    当调用 Enum.valueOf() 时,为了避免每次都使用低效的反射去遍历枚举类,Java 在底层做了一层极其精妙的高性能缓存。

    java.lang.Class 类中,维护着一个名为 enumConstantDirectory() 的包级私有方法。当某枚举类第一次被 valueOf() 解析时,该方法会利用反射获取所有的枚举实例,并构建一个 Map<String, T>(键为 name(),值为枚举实例)缓存在 Class 对象内部(使用了 volatile 保证多线程可见性)。后续所有的 valueOf() 调用,本质上都是在这个 Map 中进行 O(1)O(1) 复杂度的 get() 操作。

    java
    // java.lang.Enum.valueOf 核心源码剖析
    public static <T extends Enum<T>> T valueOf(Class<T> enumClass, String name) {
      // 触发 Class 内部的懒加载 Map 缓存机制
      T result = enumClass.enumConstantDirectory().get(name);
      if (result != null)
        return result;
    
      // 如果 Map 里没有匹配的 key,说明传入的名字不对,抛出异常
      if (name == null)
        throw new NullPointerException("Name is null");
      throw new IllegalArgumentException(
        "No enum constant " + enumClass.getCanonicalName() + "." + name);
    }

注意事项

  1. 致命的反序列化炸弹:IllegalArgumentException 满天飞

    在企业级后端开发中,最危险的反模式(Anti-Pattern)之一就是:直接将前端传入的字符串,或从外部 API/数据库读取的未知字符串,丢进 valueOf() 进行转换。

    由于 valueOf() 是严格大小写敏感且完全匹配声明变量名的,一旦外部数据存在空格、大小写不匹配,或者由于版本迭代导致枚举名被重构(例如 WECHAT 改成了 WX),valueOf() 将无情地抛出运行时异常,导致整个业务流程中断或 HTTP 请求直接返回 500 错误。

    java
    // 危险代码:直接信任外部输入
    public void processOrder(String statusStr) {
      // 如果 statusStr 传入 "paid " (带空格) 或 "Paid" (小写)
      // 会直接抛出 IllegalArgumentException,导致线程崩溃
      OrderStatus status = OrderStatus.valueOf(statusStr);
      // ... 业务逻辑
    }
  2. 与 toString() 解耦,只认 name()

    必须牢记,valueOf() 的反序列化依据**永远且仅仅是 name()**返回的值,而不是toString()。如果你重写了 toString()以返回友好的中文描述,千万不要试图把这个中文描述再塞给valueOf() 让它还原为枚举对象,这会百分之百导致找不到常量的异常。

扩展知识

  1. 高鲁棒性防御实践:自定义安全的 parse() 或 fromName() 方法

    为了彻底根除 valueOf() 带来的异常风险,高级 Java 工程师通常会在枚举内部封装一个**不抛异常的(Non-Throwing)**安全转换方法,结合 Java 8 的 Optional 或直接返回默认值,优雅地处理脏数据。

    java
    public enum PayType {
      ALIPAY, WECHAT, BANK_CARD;
    
      // 工业级安全解析方案:替代危险的 valueOf()
      public static Optional<PayType> parseSafely(String name) {
        if (name == null || name.trim().isEmpty()) {
          return Optional.empty();
        }
        try {
          // 依然使用底层高效的 valueOf,但在内部捕获异常并吞掉
          // 或者使用预热的 Map 缓存进行 getOrDefault() 避免创建 Exception 对象
          return Optional.of(PayType.valueOf(name.trim().toUpperCase()));
        } catch (IllegalArgumentException e) {
          // 记录警告日志,而不是让业务崩溃
          return Optional.empty();
        }
      }
    }
    
    // 业务调用端变得极其优雅且安全:
    PayType type = PayType.parseSafely(" alipay ").orElse(PayType.BANK_CARD);

compareTo()

public final int compareTo(E o)比较此枚举常量与指定对象的顺序,其排序依据严格由常量在源代码中的声明顺序(序数)决定。

  • oE,要与此枚举常量进行比较的同一个枚举类型的对象。
  • 返回int,如果此枚举常量的序数小于指定对象的序数,则返回负整数;如果相等,则返回零;如果大于,则返回正整数。(实际上返回的是 this.ordinal - o.ordinal 的差值)。
  • 抛出
    • NullPointerException - 如果传入的比较对象 onull
    • ClassCastException - 如果指定对象的类型与此枚举的类型不一致(通常在编译期会被泛型拦截,但在使用原始类型或反射时会在运行时触发)。

基本示例

枚举常量的自然顺序比较:直接验证枚举项在代码中声明的先后位置。

java
public enum WorkflowState {
  DRAFT,      // ordinal 0
  REVIEWING,  // ordinal 1
  APPROVED,   // ordinal 2
  PUBLISHED   // ordinal 3
}

public class Main {
  public static void main(String[] args) {
    WorkflowState state1 = WorkflowState.DRAFT;
    WorkflowState state2 = WorkflowState.APPROVED;

    // DRAFT (0) 与 APPROVED (2) 比较 -> 0 - 2 = -2
    System.out.println(state1.compareTo(state2)); // 输出负数 (-2)

    // PUBLISHED (3) 与 REVIEWING (1) 比较 -> 3 - 1 = 2
    System.out.println(WorkflowState.PUBLISHED.compareTo(WorkflowState.REVIEWING)); // 输出正数 (2)

    // 自身比较
    System.out.println(state1.compareTo(WorkflowState.DRAFT)); // 输出 0
  }
}

核心特性

  1. 基于底层 ordinal 的极简数学减法

    Enum<E> 类实现了 Comparable<E> 接口。当你调用 compareTo 时,它底层完全没有任何复杂的比较逻辑,仅仅是对两个枚举实例内部的 ordinal(序数)字段进行简单的整数减法。正因为它是直接相减,所以返回的并不总是固定的 -101,而是它们声明位置的真实索引差值。由于 ordinal 在类加载时就由编译器固定,这种比较操作拥有极致的 O(1)O(1) 性能。

  2. 匿名内部类实例的强类型校验机制 (Class vs DeclaringClass)

    这是一个非常硬核的底层细节。compareTo 的源码中有一段极其精妙的类型检查。如果枚举项重写了方法,该枚举项在运行时的 Class 实际上是一个匿名子类,此时简单的 getClass() == o.getClass() 会失效!为了保证属于同一个枚举基类的不同子类实例依然可以比较,JDK 引入了 getDeclaringClass() 进行降级校验。

    java
    // java.lang.Enum 的 compareTo 核心源码剖析
    public final int compareTo(E o) {
      Enum<?> other = (Enum<?>)o;
      Enum<E> self = this;
    
      // 【硬核校验逻辑】
      // 1. 优先校验 getClass(),这是一个极致的性能优化(如果是普通枚举,这里就直接跳过异常了)
      // 2. 如果 getClass 不同,说明其中一方或双方是带有特定方法实现的“匿名内部类”
      // 3. 此时调用 getDeclaringClass() 追溯它们的根本枚举基类,只要基类相同,依然允许比较!
      if (self.getClass() != other.getClass() &&
          self.getDeclaringClass() != other.getDeclaringClass()) {
        throw new ClassCastException();
      }
    
      // 最核心的比较逻辑:简单的序数相减
      return self.ordinal - other.ordinal;
    }
  3. 不可重写性 (Final Constraint)

    与其他许多实现 Comparable 接口的类(如 String, Date)不同,Enum 中的 compareTo 被声明为 final。这意味着你绝对无法干预枚举的自然排序机制。Java 语言规范强制规定,只要你是枚举,你的自然排序(Natural Ordering)就必须、且只能是你源代码中的物理声明顺序。

注意事项

  1. 隐式重构炸弹:脆弱的业务状态机流转

    这是企业级开发中最容易引发线上故障的灾难性反模式。许多开发者图省事,利用 compareTo 来判断业务状态的流转是否合法(例如判断订单状态是否已经过了“已支付”阶段)。

    风险点:因为 compareTo 强依赖物理声明顺序,一旦后续开发人员为了“代码归类”或者盲目新增状态,在枚举声明的中间插入了一个新状态,所有基于 compareTo 的业务判断逻辑将全部崩溃,且没有任何编译期报错

    java
    public enum OrderState {
      CREATED, PAID, DELIVERED, FINISHED
    }
    
    // 危险的业务代码:判断订单是否已经过了支付阶段
    public boolean canApplyRefund(OrderState currentState) {
      // 如果某天有新人把 REFUNDING 状态插在了 PAID 和 DELIVERED 之间...
      // 这里的逻辑就会默默地出现极其严重的 BUG
      return currentState.compareTo(OrderState.PAID) > 0;
    }
  2. 集合排序的隐式依赖

    由于 Enum 实现了 Comparable,当你将枚举放入 TreeSet 或使用 Collections.sort()Stream.sorted() 对包含枚举的列表进行排序时,它们底层都会隐式调用被 final 锁死的 compareTo()。这意味着集合的输出顺序永远受限于源码的定义顺序

扩展知识

  1. 解绑声明顺序:使用 Comparator 实现业务排序规则

    如果你的业务确实需要比较枚举的大小(例如基于权重、优先级,或者状态的实际流转顺序),**标准的高级实践是抛弃原生的 compareTo**。你应该为枚举引入一个代表优先级的显式 weightstep字段,并利用Comparator 来进行比较,从而彻底将业务逻辑与代码物理排列解耦。

    java
    public enum MemberLevel {
      // 即使源码顺序随意打乱,业务排序依然坚如磐石
      PLATINUM(30),
      SILVER(10),
      GOLD(20);
    
      private final int priority;
    
      MemberLevel(int priority) {
        this.priority = priority;
      }
    
      public int getPriority() {
        return priority;
      }
    
      // 定义业务专属的排序器
      public static final Comparator<MemberLevel> PRIORITY_COMPARATOR =
        Comparator.comparingInt(MemberLevel::getPriority);
    }
    
    // 业务调用方:
    List<MemberLevel> levels = Arrays.asList(MemberLevel.PLATINUM, MemberLevel.SILVER);
    // 不使用默认的 natural ordering,使用自定义的 Comparator
    levels.sort(MemberLevel.PRIORITY_COMPARATOR);

综合示例

我们依然以第一章中的 Season (春、夏、秋、冬) 枚举为例,来看看它可以使用哪些常用方法:

java
public enum Season {
  SPRING, SUMMER, AUTUMN, WINTER
}

public class EnumMethodDemo {
  public static void main(String[] args) {
    Season s = Season.SUMMER;

    // 1. name() 或 toString():获取枚举常量的名称
    System.out.println("name: " + s.name());         // 输出: SUMMER
    System.out.println("toString: " + s.toString()); // 输出: SUMMER
    // 注意:通常情况下两者结果一样,但 toString() 可以被重写,而 name() 是 final 的不能重写。

    // 2. ordinal():获取枚举常量的索引位置(从 0 开始计数)
    System.out.println("ordinal: " + s.ordinal());   // 输出: 1 (SPRING是0,SUMMER是1)

    // 3. values():返回包含所有枚举常量的一个数组
    // 【注意】这个方法在 java.lang.Enum 中找不到,它是编译器在编译时帮我们自动生成的。
    Season[] allSeasons = Season.values();
    System.out.println("所有的季节:");
    for (Season season : allSeasons) {
      System.out.println(season.ordinal() + " - " + season.name());
    }
    // 输出:
    // 0 - SPRING
    // 1 - SUMMER
    // 2 - AUTUMN
    // 3 - WINTER

    // 4. valueOf(String name):将字符串转换为对应的枚举常量
    // 字符串必须与枚举常量的名称完全一致(大小写敏感),否则会抛出 IllegalArgumentException
    Season winter = Season.valueOf("WINTER");
    System.out.println("字符串转换得到的枚举: " + winter); // 输出: WINTER
  }
}

底层原理与高级应用

本质:语法糖

编译的本质:枚举是一颗“语法糖”:

在 Java 中,enum 关键字本质上是一颗语法糖。JVM 底层并没有一种叫做“枚举”的特殊数据结构。当你编译一个枚举类时,Java 编译器会在背后帮你写很多代码,最终把它转换成一个普通的 Java 类

如果我们用反编译工具(如 javap)查看第一章中的 Season 枚举编译后的 .class 文件,你会发现它大概长这个样子(这里做了简化以便理解):

java
// 这是编译器真正生成的代码(伪代码)
public final class Season extends java.lang.Enum<Season> {  

  // 1. 枚举常量被转化为 public static final 的静态常量
  public static final Season SPRING;
  public static final Season SUMMER;
  public static final Season AUTUMN;
  public static final Season WINTER;

  // 2. 一个包含所有枚举实例的私有静态数组
  private static final Season[] $VALUES;

  // 3. 在静态代码块中实例化这些常量
  static {
    SPRING = new Season("SPRING", 0);
    SUMMER = new Season("SUMMER", 1);
    AUTUMN = new Season("AUTUMN", 2);
    WINTER = new Season("WINTER", 3);
    $VALUES = new Season[]{SPRING, SUMMER, AUTUMN, WINTER};
  }

  // 4. 强制私有的构造器,调用父类 Enum 的构造器
  private Season(String name, int ordinal) {
    super(name, ordinal);
  }

  // 5. 编译器自动生成的 values() 和 valueOf() 方法
  public static Season[] values() {
    return $VALUES.clone();
  }

  public static Season valueOf(String name) {
    return Enum.valueOf(Season.class, name);
  }
}

从底层源码我们可以得出以下结论:

  • 为什么枚举不能继承其他类? 因为它在编译时已经默认继承了 java.lang.Enum
  • 为什么枚举是线程安全的? 因为枚举的实例都是在 static 代码块中初始化的,而类的加载和初始化由 JVM 保证绝对的线程安全。
  • 为什么 values() 方法在官方 API 文档里找不到? 因为那是编译器在编译阶段动态强加进去的。

应用:最完美的单例模式

高级应用:最完美的单例模式:

单例模式(Singleton)是开发中最常用的设计模式之一。传统的单例模式(如双重检查锁 Double-Checked Locking)往往需要处理多线程并发、反射破坏、反序列化破坏等一堆麻烦事。

而《Effective Java》的作者 Joshua Bloch 极力推荐使用枚举来实现单例模式,这也是目前公认最安全、最简洁的单例实现方式。

java
public enum Singleton {
  INSTANCE; // 唯一实例

  // 你可以在这里定义业务逻辑所需的方法和属性
  public void doSomething() {
    System.out.println("执行单例对象的方法");
  }
}

// 调用方式:
// Singleton.INSTANCE.doSomething();

为什么枚举单例是“最完美”的?

  1. 天生线程安全:如前文所述,JVM 在加载类时通过 static 机制保证了实例创建的线程安全。

  2. 防御反射攻击:Java 的反射机制中的 Constructor.newInstance() 源码中,有一段特殊的判断:如果发现创建的类是 enum,会直接抛出 IllegalArgumentException,彻底杜绝了通过反射创建新实例的可能。

  3. 防御反序列化攻击:普通的单例在反序列化时会重新创建对象,需要重写 readResolve 方法。而枚举由 JVM 层面保证了反序列化时只会返回现有的枚举常量,绝对不会创建新对象。

EnumSetEnumMap

性能怪兽:EnumSetEnumMap:

如果你需要将枚举用作集合的元素或键,**千万不要使用普通的 HashSetHashMap**,Java 为枚举量身定制了两个极其高效的集合类。

EnumSet:处理枚举集合的最佳选择

EnumSet 底层使用的是**位向量(Bit Vector)**实现。对于少于 64 个元素的枚举,它只需要一个 long 类型的变量就能存储整个集合。它的执行效率甚至比按位运算(Bitwise operations)还要快,且内存占用极小。

java
import java.util.EnumSet;

public class EnumSetDemo {
  public static void main(String[] args) {
    // 创建一个包含特定枚举值的集合
    EnumSet<Season> springAndSummer = EnumSet.of(Season.SPRING, Season.SUMMER);

    // 创建一个包含所有枚举值的集合
    EnumSet<Season> allSeasons = EnumSet.allOf(Season.class);
  }
}

EnumMap:以枚举为键的高效 Map

EnumMap 底层直接使用**数组(Array)**来实现。由于枚举常量的索引(ordinal)是紧凑且连续的,EnumMap 将枚举的 ordinal 作为数组的下标直接进行存取。因此,它没有 HashMap 计算 Hash 值和解决 Hash 冲突的开销,查询速度是 O(1)O(1) 的绝对巅峰。

java
import java.util.EnumMap;

public class EnumMapDemo {
  public static void main(String[] args) {
    // 创建 EnumMap,必须在构造时传入枚举的 Class 对象
    EnumMap<Season, String> clothingMap = new EnumMap<>(Season.class);

    clothingMap.put(Season.SPRING, "风衣");
    clothingMap.put(Season.SUMMER, "T恤");

    System.out.println("夏天穿什么: " + clothingMap.get(Season.SUMMER));
  }
}

练习题

  1. Week 枚举类

    声明 Week 枚举类,包含星期一至星期日,使用 values()遍历输出。

    image-20260309180613840

-----------------------------

需求场景

创建季节(Season)对象,要求:

  • 季节对象固定(春、夏、秋、冬)
  • 只读,不能修改

问题分析

  1. 季节值是有限的 4 个
  2. 需要只读属性,不能修改
  3. 传统类设计无法保证对象唯一性

解决方案-枚举

  1. 枚举(enumeration,简写 enum)是一组常量的集合
  2. 枚举属于特殊类,仅包含有限的特定对象

枚举的两种实现方式

  1. 自定义类实现枚举
  2. 使用 enum 关键字实现枚举

自定义类实现枚举-应用案例

java
package com.hspedu.enum_;

/**
 * 自定义枚举类实现
 */
public class Enumeration02 {
  public static void main(String[] args) {
    System.out.println(Season.AUTUMN);
    System.out.println(Season.SPRING);
  }
}

class Season { // 枚举类
  private String name;
  private String desc; // 描述

  // 1. 私有构造器,防止直接new
  private Season(String name, String desc) {
    this.name = name;
    this.desc = desc;
  }

  // 2. 本类内部创建固定对象
  public static final Season SPRING = new Season("春天", "温暖");
  public static final Season SUMMER = new Season("夏天", "炎热");
  public static final Season AUTUMN = new Season("秋天", "凉爽");
  public static final Season WINTER = new Season("冬天", "寒冷");

  // 3. 提供get方法,无set方法(只读)
  public String getName() {
    return name;
  }

  public String getDesc() {
    return desc;
  }

  @Override
  public String toString() {
    return "Season{" +
      "name='" + name + '\'' +
      ", desc='" + desc + '\'' +
      '}';
  }
}

自定义类实现枚举-小结

  1. 构造器私有化
  2. 本类内部创建固定对象
  3. 对外暴露对象(public final static 修饰)
  4. 提供 get 方法,不提供 set 方法

enum 关键字实现枚举-快速入门

java
package com.hspedu.enum_;

/**
 * 使用enum关键字实现枚举
 */
public class Enumeration03 {
  public static void main(String[] args) {
    System.out.println(Season2.AUTUMN);
    System.out.println(Season2.SUMMER);
  }
}

// 枚举类
enum Season2 {
  // 枚举对象(必须放在行首),多个用逗号分隔,末尾可加封号
  SPRING("春天", "温暖"),
  SUMMER("夏天", "炎热"),
  AUTUMN("秋天", "凉爽"),
  WINTER("冬天", "寒冷");

  private String name;
  private String desc;

  // 构造器(私有,默认可以省略private)
  Season2(String name, String desc) {
    this.name = name;
    this.desc = desc;
  }

  // 无参构造器(如果需要)
  private Season2() {}

  // get方法
  public String getName() {
    return name;
  }

  public String getDesc() {
    return desc;
  }

  @Override
  public String toString() {
    return "Season{" +
      "name='" + name + '\'' +
      ", desc='" + desc + '\'' +
      '}';
  }
}

enum 关键字实现枚举注意事项

  1. enum 类默认继承 Enum 类,且是 final 类(不能被继承)
  2. 传统public static final Season SPRING = new Season(...)简化为SPRING(...)
  3. 无参构造器创建枚举对象时,可省略()
  4. 多个枚举对象用逗号分隔,末尾加分号
  5. 枚举对象必须放在枚举类行首

enum 关键字实现枚举-课堂练习

练习 1

java
// 代码是否正确? 含义是什么?
enum Gender{
  BOY, GIRL; // 调用无参构造器
}
  • 语法正确
  • 枚举类 Gender,无属性
  • 两个枚举对象 BOY、GIRL(无参构造器创建)

练习 2

java
enum Gender2{
  BOY,GIRL;
}

public class Test {
  public static void main(String[] args) {
    Gender2 boy = Gender2.BOY;
    System.out.println(boy); // 输出BOY(调用父类Enum的toString())
    Gender2 boy2 = Gender2.BOY;
    System.out.println(boy2 == boy); // True(同一对象)
  }
}

enum 常用方法说明

enum 类隐式继承 Enum 类,可使用以下常用方法:

方法名详细描述
valueOf传递枚举类型 Class 和常量名,返回匹配的枚举常量
toString返回当前枚举常量名称(可重写)
equals直接使用==比较,用于集合中
hashCode与 equals 保持一致,不可变
getDeclaringClass获取枚举常量所属的枚举类 Class 对象
name返回枚举常量名称(不可重写)
ordinal返回枚举常量的次序(从 0 开始)
compareTo比较两个枚举常量的次序(编号差值)
clone不可克隆,抛出 CloneNotSupportedException

方法应用实例(EnumMethod.java)

java
package com.hspedu.enum_;

/**
 * 演示Enum类常用方法
 */
public class EnumMethod {
  public static void main(String[] args) {
    Season2 autumn = Season2.AUTUMN;

    // 1. name():返回枚举对象名称
    System.out.println(autumn.name()); // AUTUMN

    // 2. ordinal():返回次序(从0开始)
    System.out.println(autumn.ordinal()); // 2

    // 3. values():返回所有枚举对象数组
    Season2[] values = Season2.values();
    System.out.println("===遍历枚举对象===");
    for (Season2 season : values) {
      System.out.println(season);
    }

    // 4. valueOf():将字符串转为枚举对象
    Season2 autumn1 = Season2.valueOf("AUTUMN");
    System.out.println("autumn1=" + autumn1);
    System.out.println(autumn == autumn1); // True

    // 5. compareTo():比较次序差值
    System.out.println(Season2.AUTUMN.compareTo(Season2.SUMMER)); // 2-3=-1
  }
}

enum 常用方法课堂练习

需求

声明 Week 枚举类,包含星期一至星期日,使用 values()遍历输出。

代码实现

java
package com.hspedu.enum_;

public class EnumExercise02 {
  public static void main(String[] args) {
    // 获取所有枚举对象
    Week[] weeks = Week.values();
    System.out.println("===所有星期的信息如下===");
    for (Week week : weeks) {
      System.out.println(week);
    }
  }
}

enum Week {
  MONDAY("星期一"),
  TUESDAY("星期二"),
  WEDNESDAY("星期三"),
  THURSDAY("星期四"),
  FRIDAY("星期五"),
  SATURDAY("星期六"),
  SUNDAY("星期日");

  private String name;

  // 构造器
  private Week(String name) {
    this.name = name;
  }

  // 重写toString()
  @Override
  public String toString() {
    return name;
  }
}

enum 实现接口

  1. enum 类不能继承其他类(已隐式继承 Enum)
  2. 枚举类可以实现接口,与普通类实现接口语法一致

代码示例

java
package com.hspedu.enum_;

public class EnumDetail {
  public static void main(String[] args) {
    Music.CLASSICMUSIC.playing();
  }
}

// 接口
interface IPlaying {
  void playing();
}

// 枚举类实现接口
enum Music implements IPlaying {
  CLASSICMUSIC;

  @Override
  public void playing() {
    System.out.println("播放好听的音乐...");
  }
}